Building a MAUI CustomRefreshView

Using MAUI for creating a real application presents a few real challenges. Creating a multiplatform application is not fully supported, unlike the RefreshView component.

.NET MAUI is a powerful cross-platform UI framework, but not all controls are supported on every platform. One notable limitation is the absence of RefreshView support on macOS. This can be a blocker for developers who want a consistent pull-to-refresh experience across all platforms.

In this post, I will:

  • Explain the issue with RefreshView on macOS.
  • Show how to build a cross-platform CustomRefreshView for .NET MAUI.
  • Provide step-by-step code and XAML integration.

The full source code of this component is available on GitHub.

The Problem: RefreshView on macOS

The built-in RefreshView control in .NET MAUI enables pull-to-refresh functionality for scrollable content. However, as of .NET MAUI 9, RefreshView is not supported on macOS. Attempting to use it will result in runtime errors or simply no refresh gesture support.

Why does this happen?

  • The underlying gesture and native control implementation for RefreshView is missing on macOS.
  • The official documentation and GitHub issues confirm this limitation.

Impact

  • macOS users cannot trigger refresh actions via pull gestures.
  • UI consistency and user experience are affected.

Here some Microsoft documentation about it:

Warning

UIStepper, UIPickerView, and UIRefreshControl aren’t supported in the Mac user interface idiom by Apple. This means that the .NET MAUI controls that consume these native controls are not usable in the Mac user interface idiom. These include the Stepper, Picker, and RefreshView. Attempting to do so will throw a macOS exception.

In addition, the following constraints apply in the Mac user interface idiom:

Solution: Build a CustomRefreshView

To overcome this, you can create a custom control. It should mimic the core features of RefreshView and work on all platforms. This includes macOS.

Features to Implement

  • Pull-to-refresh gesture detection
  • Visual feedback (spinner and optional text)
  • Command execution on refresh
  • Bindable properties for customization (color, position, background, etc.)

Step 1: Create the CustomRefreshView Class

Create a new file CustomRefreshView.cs in your MAUI project.

using System; using Microsoft.Maui.Controls;
namespace YourApp.Components { public enum Position { Top, Middle, Bottom }
public class CustomRefreshView : ContentView
{
    // Bindable properties for IsRefreshing, RefreshCommand, RefreshColor, etc.
    // See full code below for all properties

    // Internal controls
    private readonly ActivityIndicator _activityIndicator;
    private readonly Label _indicatorLabel;
    private readonly Grid _grid;
    private readonly VerticalStackLayout _indicatorStack;
    private double _totalY;

    public CustomRefreshView()
    {
        // Gesture detection
        var panGesture = new PanGestureRecognizer();
        panGesture.PanUpdated += OnPanUpdated;
        GestureRecognizers.Add(panGesture);

        // Spinner
        _activityIndicator = new ActivityIndicator
        {
            IsVisible = false,
            IsRunning = false,
            VerticalOptions = LayoutOptions.Center,
            HorizontalOptions = LayoutOptions.Center,
            InputTransparent = true
        };
        _activityIndicator.SetBinding(ActivityIndicator.ColorProperty, new Binding(nameof(RefreshColor), source: this));

        // Optional text
        _indicatorLabel = new Label
        {
            IsVisible = false,
            VerticalOptions = LayoutOptions.Center,
            HorizontalOptions = LayoutOptions.Center
        };

        // Stack for spinner and text
        _indicatorStack = new VerticalStackLayout
        {
            HorizontalOptions = LayoutOptions.Center,
            VerticalOptions = LayoutOptions.Center,
            IsVisible = false,
            Children = { _activityIndicator, _indicatorLabel }
        };

        // Grid for positioning
        _grid = new Grid
        {
            RowDefinitions =
            {
                new RowDefinition { Height = GridLength.Star },
                new RowDefinition { Height = GridLength.Star },
                new RowDefinition { Height = GridLength.Star }
            }
        };
        _grid.Children.Add(_indicatorStack);
        Grid.SetRow(_indicatorStack, 1); // Default: Middle

        Content = _grid;
    }

    // ... Bindable properties and property changed handlers ...
    // See full code below
}

Step 2: Add Bindable Properties

Add properties for customization and MVVM support:

  • IsRefreshing (bool)
  • RefreshCommand (Command)
  • RefreshColor (Color)
  • IndicatorText (string)
  • IndicatorTextColor (Color)
  • IndicatorBackground (Color)
  • IndicatorPosition (enum: Top, Middle, Bottom)
  • IndicatorMargin, IndicatorMinimumWidthRequest, IndicatorMinimumHeightRequest (layout)

This is an example for the IsRefreshing property

public static readonly BindableProperty IsRefreshingProperty =
    BindableProperty.Create(
        nameof(IsRefreshing),
        typeof(bool),
        typeof(CustomRefreshView),
        false,
        propertyChanged: OnIsRefreshingChanged);

public bool IsRefreshing
{
    get => (bool)GetValue(IsRefreshingProperty);
    set => SetValue(IsRefreshingProperty, value);
}

Step 3: Handle the Pull-to-Refresh Gesture

Detect a downward pan gesture and trigger the refresh command:

private void OnPanUpdated(object sender, PanUpdatedEventArgs e)
{
    switch (e.StatusType)
    {
        case GestureStatus.Started:
            _totalY = 0;
            break;

        case GestureStatus.Running:
            _totalY += e.TotalY;
            break;

        case GestureStatus.Completed:
            if (_totalY > 50)
                RefreshCommand?.Execute(null);
            break;
    }
}

Step 4: Show Spinner and Text When Refreshing

Update visibility and appearance based on IsRefreshing and other properties:

private static void OnIsRefreshingChanged(BindableObject bindable, object oldValue, object newValue)
{
    if (bindable is CustomRefreshView control)
    {
        bool isRefreshing = (bool)newValue;
        control._activityIndicator.IsRunning = isRefreshing;
        control._activityIndicator.IsVisible = isRefreshing;
        control._indicatorLabel.IsVisible = isRefreshing && !string.IsNullOrEmpty(control.IndicatorText);
        control._indicatorStack.IsVisible = isRefreshing;
    }
}

Step 5: Position the Indicator Group

Use the grid to position the indicator at the top, middle, or bottom:

private static void OnIndicatorPositionChanged(BindableObject bindable, object oldValue, object newValue)
{
    var control = (CustomRefreshView)bindable;
    var position = (Position)newValue;

    switch (position)
    {
        case Position.Top:
            control._indicatorStack.VerticalOptions = LayoutOptions.Start;
            break;

        case Position.Bottom:
            control._indicatorStack.VerticalOptions = LayoutOptions.End;
            break;

        default:
            control._indicatorStack.VerticalOptions = LayoutOptions.Center;
            break;
    }
}

Step 6: Use the CustomRefreshView in XAML

<components:CustomRefreshView
	IndicatorBackground="{StaticResource Gray100}"
	IndicatorPosition="Top"
	IndicatorText="Loading data..."
	IndicatorTextColor="{StaticResource OffBlack}"
	IsRefreshing="{Binding IsRefreshing}"
	RefreshColor="{AppThemeBinding Light={StaticResource Primary10},
			                        Dark={StaticResource Primary10}}"
	RefreshCommand="{Binding RefreshCommand}">
	<components:CustomRefreshView.RefreshContent>

    <!-- Add here the content of the page -->

	</components:CustomRefreshView.RefreshContent>
</components:CustomRefreshView>

Conclusion

By building a CustomRefreshView, you can provide a consistent pull-to-refresh experience across all .NET MAUI platforms, including macOS. This approach is flexible, customizable, and future-proof for your cross-platform applications.

Key takeaways:

  • RefreshView is not supported on macOS in .NET MAUI.
  • A custom control can replicate its functionality using gesture detection and MVVM-friendly properties.
  • The solution is fully cross-platform and highly customizable.

Happy coding!

Related posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.